﻿/****************************** Module Header ******************************\
Project:            CrmRestKit

Project Source:     https://crmrestkit.codeplex.com
Version:            3.0.0-beta
Credits:
    - ajax /encode logic: https://github.com/stackp/promisejs/blob/master/promise.js

This source is subject to the Microsoft Public License.
See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL.
All other rights reserved.

THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
\***************************************************************************/
module RestKit {
    export interface Guid extends String { }
    /** Collection result */
    export interface IRestQueryResult<T> {
        d: {
            results: T[];
            __next: string;
        };
    }
    /** Single Item result */
    export interface IRestItemResult<T> {
        d: T;
    }
    /** Marker interface for a rest entity
     * For future use
     */
    export interface IRestEntity {

    }

    /** Interface defining the method required on the XMLHttpRequest object used to perform the request. */
    export interface IXhr {
        open(method: string, url: string, async?: boolean, user?: string, password?: string): void;
        setRequestHeader(header: string, value: string): void;
        send(data?: any): void;
        onreadystatechange: (ev: Event) => any;
        responseBody: any;
        status: number;
        readyState: number;
        responseText: string;
    }
    /** Promise contract */
    export interface IPromise<TValue> {
        /** 
         * Promise patern method
         * @param {delegate} fulfilled Method called when the promise is fulfilled successfully
         * @param {delegate} rejected Method called when the promise is not fulfilled;
         * @returns {RestKit.IPromise} 
         */
        then<TReturn>(fulfilled: { (value: TValue): TReturn }, rejected: { (reason: any) }): IPromise<TReturn>;
        //done(func: { (value: TValue): void }): IPromise<TValue>;
        //fail(func: { (reason: any): void }): IPromise<TValue>;
        //always(func: { (value: any): void }): IPromise<TValue>;
    }
    /** Defer contract */
    export interface IDefer<TValue> {
        /** 
         * The promise cannot be fulfilled successfully
         * @param {any} reason
         */
        reject(reason: any): void;
        resolve(value: TValue): void;
        notify(message: any): void;

        promise: IPromise<TValue>;
    }

    export interface IDeferProvider { <T>(): IDefer<T> }
    export interface IXhrProvider { (): IXhr }
    export interface IXrmContextProvider { (): XrmTypes.Context }

    /** A very simple promise provider */
    export class VerySimplePromise<TValue> implements IDefer<TValue>, IPromise<TValue> {
        _fulfilled: { (value: TValue): void } = null;
        _rejected: { (reason: any) } = null;
        state: string = 'pending';
        resolveValue: any;

        child: VerySimplePromise<TValue>;

        then(fulfilled: { (value: TValue): void }, rejected: { (reason: any) }): IPromise<TValue> {
            this._fulfilled = fulfilled;
            this._rejected = rejected;
            this.child = new VerySimplePromise<TValue>();

            //If the promise is already resolved execute it immediately
            if (this.state == 'resolve')
                this.resolve(this.resolveValue);
            else if (this.state == 'reject')
                this.reject(this.resolveValue);

            return this.child;
        }

        //done(func: { (value: TValue): void }): IPromise<TValue> {
        //    this._fulfilled.push(func);
        //    return this;
        //}
        //fail(func: { (reason: any): void }): IPromise<TValue> {
        //    this._rejected.push(func);
        //    return this;
        //}
        //always(func: { (value: any): void }): IPromise<TValue> {
        //    this._fulfilled.push(func);
        //    this._rejected.push(func);
        //    return this;
        //}
        reject(reason: any): void {
            this.resolveValue = reason;

            this.state = 'reject';
            if (this._rejected)
                this._rejected(this.resolveValue);
            if (this.child)
                this.child.reject(this.resolveValue);
        }

        resolve(value: TValue): void {
            this.resolveValue = value;
            this.state = 'resolve';
            try {
                if (this._fulfilled)
                    this._fulfilled(this.resolveValue);
                if (this.child)
                    this.child.resolve(this.resolveValue);
            }
            catch (e) {
                this.reject(e);
            }
        }
        notify(message: any): void {
        }

        promise: IPromise<TValue> = this;
    }

    /** The restkit it self */
    export class RestKit {

        private _deferFactory: IDeferProvider
        private _xhrProvider: IXhrProvider;
        private _contextProvider: IXrmContextProvider;
        /** Create a new RestKit with dependencies
         * @constructor
         * @param {delegate} defer - Method used to instaniate the promise
         * @param {delegate} [xhrProvider] - Method used to instantiate the Http Request Object. 
         * @param {delegate} [contextProvider] - Method used to get the Xrm context object. 
         */
        constructor(defer: IDeferProvider,
            xhrProvider: IXhrProvider = RestKit.getXhrInstance,
            contextProvider: IXrmContextProvider = RestKit.getContext) {
            this._deferFactory = defer;
            this._xhrProvider = xhrProvider;
            this._contextProvider = contextProvider;
        }

        version(): string {
            return '3.0.0-beta';
        }

        /** Retrieve a record
         * @param {string} entityName Schema name of the entity
         * @param {string} id Id of the record to retrieve
         * @param {string[]} columns list of columns
         * @param {boolean} [async=true] Execute asynchrously
         */
        Retrieve<TEntity>(entityName: string, id: Guid, columns: string[]): IPromise<IRestItemResult<TEntity>> {
            var url = this.getODataPath() + "/" + entityName + "Set(guid'" + id + "')" + "?$select=" + columns.join(',');

            return this.ajax<IRestItemResult<TEntity>>('GET', url);
        }
        RetrieveSync<TEntity>(entityName: string, id: Guid, columns: string[]): IRestItemResult<TEntity> {
            var url = this.getODataPath() + "/" + entityName + "Set(guid'" + id + "')" + "?$select=" + columns.join(',');

            return this.ajaxSync<IRestItemResult<TEntity>>('GET', url);
        }

        /** Create a record 
         * @param {string} entityName Schema name of the entity
         * @param {TEntity} entityObject The entity to be created
         * @param {boolean} [async=true] Execute asynchrously
         */
        Create<TEntity>(entityName: string, entityObject: TEntity): IPromise<IRestItemResult<TEntity>> {

            // default is 'true'
            var setName = entityName + 'Set',
                data = JSON.stringify(entityObject),
                url = this.getODataPath() + "/" + setName;

            // returns a promise object
            return this.ajax<IRestItemResult<TEntity>>('POST', url, data);
        }
        CreateSync<TEntity>(entityName: string, entityObject: TEntity): IRestItemResult<TEntity> {

            // default is 'true'
            var setName = entityName + 'Set',
                data = JSON.stringify(entityObject),
                url = this.getODataPath() + "/" + setName;

            // returns a promise object
            return this.ajaxSync<IRestItemResult<TEntity>>('POST', url, data);
        }
        /** Update a record
         * @param {string} entityName Schema name of the entity
         * @param {string} id Id of the record to update
         * @param {TEntity} entityObject The entity to be updated
         * @param {boolean} [async=true] Execute asynchrously
         */
        Update<TEntity>(entityName: string, id: Guid, entityObject: TEntity): IPromise<IRestItemResult<TEntity>> {
            var setName = entityName + 'Set',
                url = this.getODataPath() + "/" + setName + "(guid'" + id + "')",
                data = JSON.stringify(entityObject),
                headers = {
                    'X-HTTP-Method': 'MERGE'
                };

            // MERGE methode does not return data
            return this.ajax('POST', url, data, headers);
        }
        UpdateSync<TEntity>(entityName: string, id: Guid, entityObject: TEntity): IRestItemResult<TEntity> {
            var setName = entityName + 'Set',
                url = this.getODataPath() + "/" + setName + "(guid'" + id + "')",
                data = JSON.stringify(entityObject),
                headers = {
                    'X-HTTP-Method': 'MERGE'
                };

            // MERGE methode does not return data
            return this.ajaxSync<IRestItemResult<TEntity>>('POST', url, data, headers);
        }
        /** Delete a record
         * @param {string} entityName Schema name of the entity
         * @param {string} id Id of the record to delete
         * @param {boolean} [async=true] Execute asynchrously
         */
        Delete(entityName: string, id: Guid): IPromise<any> /* Return TBD */ {
            var setName = entityName + 'Set',
                url = this.getODataPath() + '/' + setName + "(guid'" + id + "')",
                headers = {
                    'X-HTTP-Method': 'DELETE'
                };

            return this.ajax('POST', url, null, headers);
        }
        DeleteSync(entityName: string, id: Guid): any /* Return TBD */ {
            var setName = entityName + 'Set',
                url = this.getODataPath() + '/' + setName + "(guid'" + id + "')",
                headers = {
                    'X-HTTP-Method': 'DELETE'
                };

            return this.ajaxSync('POST', url, null, headers);
        }
        /** Associate two records using a M:N relation
         * @param {string} entity1Id Id of record 1
         * @param {string} entity1Name Schema name of entity 1
         * @param {string} entity2Id Id of record 2
         * @param {string} entity2Name Schema name of entity 2
         * @param {string} relationshipName Schema name of the relation
         * @param {boolean} [async=true] Execute asynchrously
         */
        Associate<TEntity>(entity1Id: Guid, entity1Name: string, entity2Id: Guid, entity2Name: string, relationshipName: string): IPromise<IRestItemResult<TEntity>> {
            var odatapath = this.getODataPath(),
                url = odatapath + "/" + entity1Name + "Set(guid'" + entity1Id + "')/$links/" + relationshipName,
                data = JSON.stringify({
                    uri: odatapath + "/" + entity2Name + "Set(guid'" + entity2Id + "')"
                });

            return this.ajax('POST', url, data);

        }
        AssociateSync<TEntity>(entity1Id: Guid, entity1Name: string, entity2Id: Guid, entity2Name: string, relationshipName: string): IRestItemResult<TEntity> {
            var odatapath = this.getODataPath(),
                url = odatapath + "/" + entity1Name + "Set(guid'" + entity1Id + "')/$links/" + relationshipName,
                data = JSON.stringify({
                    uri: odatapath + "/" + entity2Name + "Set(guid'" + entity2Id + "')"
                });

            return this.ajaxSync<IRestItemResult<TEntity>>('POST', url, data);

        }
        /** Disassociate two records from a M:N relation
        * @param {string} entity1Id Id of record 1
        * @param {string} entity1Name Schema name of entity 1
        * @param {string} entity2Id Id of record 2
        * @param {string} relationshipName Schema name of the relation
        * @param {boolean} [async=true] Execute asynchrously
        */
        Disassociate<TEntity>(entity1Id: Guid, entity1Name: string, entity2Id: Guid, relationshipName: string): IPromise<IRestItemResult<TEntity>> {

            var url = this.getODataPath() + "/" + entity1Name + "Set(guid'" + entity1Id + "')/$links/" + relationshipName + "(guid'" + entity2Id + "')",
                headers = {
                    'X-HTTP-Method': 'DELETE'
                };

            return this.ajax('POST', url, null, headers);
        }
        DisassociateSync<TEntity>(entity1Id: Guid, entity1Name: string, entity2Id: Guid, relationshipName: string): IRestItemResult<TEntity> {

            var url = this.getODataPath() + "/" + entity1Name + "Set(guid'" + entity1Id + "')/$links/" + relationshipName + "(guid'" + entity2Id + "')",
                headers = {
                    'X-HTTP-Method': 'DELETE'
                };

            return this.ajaxSync<IRestItemResult<TEntity>>('POST', url, null, headers);
        }

        /** Retrieve records based on the filter
         * @param {string} entityName Schema name of the entity
         * @param {string[]} columns Schema names of attributes to retrieve
         * @param {string} filter Filter
         * @param {boolean} [async=true] Execute asynchrously
         */
        ByQuery<TEntity>(entityName: string, columns: string[], filter: string): IPromise<IRestQueryResult<TEntity>> {

            // default is 'true'
            var setName = entityName + 'Set',
                url = null;

            // in case filter is empty 
            filter = (filter) ? "&$filter=" + encodeURIComponent(filter) : '';
            url = this.getODataPath() + "/" + setName + "?$select=" + columns.join(',') + filter;

            return this.ajax('GET', url);
        }
        /** Retrieve records based on a full ODATA url string. Mainly used to page results.
         * @param {string} queryUrl The query string
         * @param {boolean} [async=true] Execute asynchrously
         */
        ByQueryUrl<TEntity>(queryUrl: string): IPromise<IRestQueryResult<TEntity>> {
            return this.ajax('GET', queryUrl);
        }
        ByQueryUrlSync<TEntity>(queryUrl: string): IRestQueryResult<TEntity> {
            return this.ajaxSync<IRestQueryResult<TEntity>>('GET', queryUrl);
        }
        /** Retrieve records based on the filter including related entities
         * @param {string} entityName Schema name of the entity
         * @param {string[]} columns Schema names of attributes to retrieve
         * @param {string} expand Relations to expand
         * @param {string} filter Filter
         * @param {boolean} [async=true] Execute asynchrously
         */
        ByExpandQuery<TEntity>(entityName: string, columns: string[], expand: string, filter: string): IPromise<IRestQueryResult<TEntity>> {

            var setName = entityName + 'Set',
                url = null;

            // in case filter is empty 
            filter = (filter) ? "&$filter=" + encodeURIComponent(filter) : '';

            url = this.getODataPath() + "/" + setName + "?$select=" + columns.join(',') + '&$expand=' + expand + filter;

            return this.ajax('GET', url);
        }
        ByExpandQuerySync<TEntity>(entityName: string, columns: string[], expand: string, filter: string): IRestQueryResult<TEntity> {

            var setName = entityName + 'Set',
                url = null;

            // in case filter is empty 
            filter = (filter) ? "&$filter=" + encodeURIComponent(filter) : '';

            url = this.getODataPath() + "/" + setName + "?$select=" + columns.join(',') + '&$expand=' + expand + filter;

            return this.ajaxSync<IRestQueryResult<TEntity>>('GET', url);
        }

        /** Retrieve all pages of the records based on the filter
         * @param {string} entityName Schema name of the entity
         * @param {string[]} columns Schema names of attributes to retrieve
         * @param {string} filter Filter
         * @param {boolean} [async=true] Execute asynchrously
         */
        ByQueryAll<TEntity>(entityName: string, columns: string[], filter: string): IPromise<TEntity[]> {

            var self = this,
                dfdAll = this._deferFactory(),
                allRecords = [];

            this.ByQuery(entityName, columns, filter).then(function byQueryAllSuccess(result) {

                // add the elements to the collection
                allRecords = allRecords.concat(result.d.results);

                if (result.d.__next) {

                    // the success-handler will be this function
                    self.ByQueryUrl(result.d.__next).then(byQueryAllSuccess, dfdAll.reject);

                    // call the progressCallbacks of the promise
                    dfdAll.notify(result);
                }
                else {
                    dfdAll.resolve(allRecords);
                }

            }, dfdAll.reject);

            return dfdAll.promise;
        }


        //#region Server Url
        /**
         * Default method used to get the Xrm Context. This automatically uses Xrm.Page.context
         * or GetGlobalContext.
         */
        private static getContext(): XrmTypes.Context {

            if (typeof Xrm !== 'undefined') {

                return Xrm.Page.context;
            }
            else if (typeof GetGlobalContext !== 'undefined') {
                /*ignore jslint start*/
                return GetGlobalContext();
                /*ignore jslint end*/
            }
            else {
                throw new Error("Context is not available.");
            }
        }
        /**
         * Returns the server-url based on the current dynamics crm version
         */
        private getServerUrl(): string {

            var localServerUrl = window.location.protocol + '//' + window.location.host;
            var context = this._contextProvider();

            if (typeof context.getClientUrl == 'function') {
                // since version SDK 5.0.13 
                // http://www.magnetismsolutions.com/blog/gayan-pereras-blog/2013/01/07/crm-2011-polaris-new-xrm.page-method

                return context.getClientUrl();
            }
            else if (context.isOutlookClient() && !context.isOutlookOnline()) {
                return localServerUrl;
            }
            else {
                var url = context.getServerUrl();
                url = url.replace(/^(http|https):\/\/([_a-zA-Z0-9\-\.]+)(:([0-9]{1,5}))?/, localServerUrl);
                url = url.replace(/\/$/, "");

                return url;
            }
        }
        /**
         * @const
         */
        private ODATA_ENDPOINT = '/XRMServices/2011/OrganizationData.svc';
        private oDataPathCache: string = null;
        /**
         * Returns the joined server-url and the endpoint-url
         */
        private getODataPath(): string {

            if (this.oDataPathCache == null) {

                this.oDataPathCache = this.getServerUrl() + this.ODATA_ENDPOINT;
            }

            return this.oDataPathCache;
        }
        //#endregion

        private static getXhrInstance(): IXhr {
            var xhr;

            if ((<any>window).XMLHttpRequest) {

                xhr = new XMLHttpRequest();
            }
            else if ((<any>window).ActiveXObject) {

                try {
                    xhr = new ActiveXObject('Msxml2.XMLHTTP');
                }
                catch (e) {

                    xhr = new ActiveXObject('Microsoft.XMLHTTP');
                }
            }
            return xhr;
        }

        /**
         * Returns all properties of the parameter as key/value pairs in a queryString.
         * @param {any} data Object to use the parameters from.
         */
        private encode(data: any): string {
            var result = '';

            if (typeof data === 'string') {
                result = data;
            }
            else {

                var e = encodeURIComponent;

                for (var key in data) {

                    if (data.hasOwnProperty(key)) {

                        result += '&' + e(key) + '=' + e(data[key]);
                    }
                }
            }
            return result;
        }

        /**
         * Internal method to perform the actual service call
         */
        private setupXhr(method: string, url: string, async: boolean = true, headers: any = {}): IXhr {
            var xhr: IXhr;

            xhr = this._xhrProvider();

            xhr.open(method, url, async);
            xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
            xhr.setRequestHeader('Accept', 'application/json');

            for (var h in headers) {

                if (headers.hasOwnProperty(h)) {

                    xhr.setRequestHeader(h, headers[h]);
                }
            }

            return xhr;
        }

        private getResult(xhr: IXhr): ExecutionResult {
            if (xhr.readyState === 4) {

                var error = (!xhr.status ||
                    (xhr.status < 200 || xhr.status >= 300) &&
                    xhr.status !== 304);

                if (error) {
                    var reason = xhr.responseText;
                    try {
                        reason = JSON.parse(xhr.responseText);
                    }
                    catch (e) { }

                    return {
                        success: false,
                        result: reason
                    };
                }
                else {

                    return {
                        success: true,
                        result: JSON.parse(xhr.responseText)
                    };
                }
            }
        }
        ajax<T>(method: string, url: string, data: any = {}, headers: any = {}): IPromise<T> {

            var deferred = this._deferFactory<T>();
            var payload = this.encode(data);
            if (method == 'GET' && payload) {
                url += '?' + payload;
                payload = null;
            }
            var xhr: IXhr;
            try {
                xhr = this.setupXhr(method, url, true, headers);
            } catch (e) {

                deferred.reject(e);
                return;
            }

            var self = this;
            xhr.onreadystatechange = function () {
                var result = self.getResult(xhr);
                if (result.success)
                    deferred.resolve(result.result);
                else
                    deferred.reject(result.result);
            };

            xhr.send(payload);

            return deferred.promise;
        }
        ajaxSync<T>(method: string, url: string, data: any = {}, headers: any = {}): T {

            var payload = this.encode(data);
            if (method == 'GET' && payload) {
                url += '?' + payload;
                payload = null;
            }
            var xhr: IXhr;
            xhr = this.setupXhr(method, url, false, headers);

            var self = this;

            xhr.send(payload);

            var result = self.getResult(xhr);

            if (result.success)
                return result.result;
            else
                throw new Error(result.result);
        }
    }
    class ExecutionResult {
        success: boolean;
        result: any;
    }
    //TBD provide typed versions?
    /** 
     *  RestKit with typed results using JQuery Defer
     */
    export class RestKitJQuery extends RestKit {
        Retrieve<TEntity>(entityName: string, id: Guid, columns: string[]): JQueryDeferred<IRestItemResult<TEntity>> {
            return <JQueryDeferred<IRestItemResult<TEntity>>>super.Retrieve(entityName, id, columns);
        }
    }

    /** create a new RestKit using JQuery as the promise provider */
    export function createJQuery($: JQueryStatic): RestKitJQuery {
        return new RestKitJQuery(<TValue>() => new JQueryDeferAdaptor(($).Deferred<TValue>()));
    }
    /** create a new RestKit using Q as the promise provider */
    export function createQ(): RestKit {
        return new RestKit(<TValue>() => Q.defer<TValue>());
    }
    /** Create a new RestKit using any promise provider */
    export function createOther(createDefer: IDeferProvider): RestKit {
        return new RestKit(createDefer);
    }
    /** Create a new RestKit using Bluebird as the promise provider */
    export function createBlueBird(xhr?: IXhrProvider): RestKit {
        return new RestKit(<TValue>() => new BlueBirdAdaptor(Promise.defer<TValue>()), xhr);
    }
    /** Use the first available promise provider (BlueBird, Q, JQuery, VerySimple)*/
    export function create(): RestKit {
        if (typeof Promise != 'undefined')
            return createBlueBird();
        else if (typeof Q != 'undefined' && Q.defer)
            return createQ();
        else if (typeof jQuery != 'undefined')
            return createJQuery(jQuery);
        else
            return createOther(() => new VerySimplePromise());

        throw new Error('No promise provider available');
    }

    /** Maps notify to progress  */
    export class BlueBirdAdaptor<TValue> implements IDefer<TValue>{
        private defer: Promise.Resolver<TValue>;

        constructor(defer: Promise.Resolver<TValue>) {
            this.defer = defer;
            this.promise = defer.promise
        }
        reject(reason: any): void {
            this.defer.reject(reason);
        }
        resolve(reason: TValue): void {
            this.defer.resolve(reason);
        }
        notify(message: any): void {
            this.defer.progress(message);
        }
        promise: IPromise<TValue>
    }
    /** Add the promise property */
    export class JQueryDeferAdaptor<TValue> implements IDefer<TValue> {
        private defer: JQueryDeferred<TValue>

        constructor(defer: JQueryDeferred<TValue>) {
            this.defer = defer;
            this.promise = defer;
        }
        reject(reason: any): IDefer<TValue> {
            return new JQueryDeferAdaptor(this.defer.reject(reason));
        }
        resolve(reason: any): IDefer<TValue> {
            return new JQueryDeferAdaptor(this.defer.resolve(reason));
        }
        notify(message: any): void {
            this.defer.notify(message);
        }
        promise: IPromise<TValue>
    }

    export interface EntityReference {
        Id: Guid;
        LogicalName: string;
        Name: string;
        __metadata: { type: string };
    }
    export interface OptionSetValue {
        Value: number;
        __metadata: { type: string };
    }
    export interface Money {
        Value: number;
        __metadata: { type: string };
    }
    export module util {

        /**
         * Returns an object that reprensts a entity-reference 
         */
        export function entityReferenceFactory(id: Guid, logicalName: string = null): EntityReference {
            if (id !== undefined && id !== null) {

                return {
                    Id: id,
                    __metadata: { type: 'Microsoft.Crm.Sdk.Data.Services.EntityReference' },
                    LogicalName: logicalName,
                    Name: null
                };
            }

            return null;
        }

        /**
         * Creates an object that reprensts a option-set-value
         */
        export function optionSetValueFactory(value: number): OptionSetValue {

            return {
                Value: value,
                __metadata: { type: 'Microsoft.Crm.Sdk.Data.Services.OptionSetValue' }
            };
        }

        /**
         * Returns an object that represents an money value
         */
        export function moneyValueFactory(value: number): Money {

            return {
                Value: value,
                __metadata: { type: 'Microsoft.Crm.Sdk.Data.Services.Money' }
            };
        }

        //#endregion

        /**
         * Parses the ODATA date-string into a date-object
         * All queries return a date in the format "/Date(1368688809000)/"
         */
        export function parseODataDate(value: string): Date {

            return new Date(parseInt(value.replace('/Date(', '').replace(')/', ''), 10));
        }
    }

    //if (typeof Xrm !== 'undefined') {
    //    var CrmRestKit = RestKit.create();
    //    (<any>CrmRestKit).util = RestKit.util;
    //}


}
export =RestKit;